package com.yys.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yys.gulimall.product.service.CategoryBrandRelationService;
import com.yys.gulimall.product.vo.web.Catelog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yys.common.utils.PageUtils;
import com.yys.common.utils.Query;

import com.yys.gulimall.product.dao.CategoryDao;
import com.yys.gulimall.product.entity.CategoryEntity;
import com.yys.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 1查出所有分类
        List<CategoryEntity> list = baseMapper.selectList(null);
        //2 组装父子树形结构

        //2.1 找到一级分类 --> 一级分类的父id是0
        //2.2 找到一级分类的子分类 --> 给entity添加一个子分类的list,


        return list.stream().filter(
                categoryEntity -> categoryEntity.getParentCid() == 0
        ).peek(categoryEntity -> categoryEntity.setChildren(getChildren(categoryEntity,list)))
                .sorted(Comparator.comparingInt(a -> (a.getSort() == null ? 0 : a.getSort()))).collect(Collectors.toList());
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO 1. 检查当前删除的菜单是否被别的地方引用
        //使用逻辑删除 show_status
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        ArrayList<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }


    /**
     * 级联更新所有关联的数据
     * 更新后失效缓存
     * ps 注意若只指定value不会生效要么指定key 要么指定allEntries为true
     * */
    @Override
    @Transactional
    @CacheEvict(value = "category",allEntries = true)
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }


    /**
     * 使用spring cache
     * 使用@Cacheable 表示需要缓存,将缓存结果放入缓存
     * 需要指定缓存的分区名字,按照业务类型分
     *
     * key 接受一个spel语句 指定redis中key的名字
     *
     * 配置文件中指定存活时间
     *
     * 自定义manager的构造器来更改默认存储value的方式(默认使用java序列化,改成使用json)
     * */
    @Cacheable(value = {"category"},key = "#root.methodName",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys...");
        return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
    }

    @Cacheable(value = {"category"},key = "#root.methodName",sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        /*
         * 1. 设置空结果缓存: 解决缓存穿透
         * 2. 设置过期时间加随机值 : 解决缓存雪崩
         * 3. 加锁: 解决缓存击穿
         *
         * */

//        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
//
//        Map<String, List<Catelog2Vo>> catalogJsonMap;
//        if(StringUtils.isBlank(catalogJson)) {
//            //缓存没有 向db查询
//            catalogJsonMap = getCatalogJsonFromDB();
//
//        } else {
//            // 逆转为对象 复杂类型使用typerefrence
//            catalogJsonMap =  JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
//        }
//
//        return catalogJsonMap;

        /**
         * 方法2.使用spring cache注解
         * */
        return getCatalogJsonFromDBDataWithSpringCache();
    }

    // 使用redis进行分布式锁
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromRedisDB() {
        /*
          将多次查询数据库改为查询一次
          加锁防止缓存击穿
          */
        synchronized (this) {
            //再查一次数据库保证第二次进来的线程不会再查数据库了
            // TODO 本地锁只能锁住本进程 --> 分布式锁
            // 设置过期时间
            String token = UUID.randomUUID().toString();
            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(
                    "lock", token, 30, TimeUnit.SECONDS);

            if(lock) {
                // 获得分布式锁成功 设置过期时间
                // 过期时间需要和加锁是原子的
                // stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
                log.info("redis分布式锁加锁成功");
                Map<String, List<Catelog2Vo>> catalogJsonFromDBData = getCatalogJsonFromDBData();
                //执行完之后释放锁 先判断是不是自己的锁
                //这里还是有问题,需要查询和删除是原子的, 使用lua脚本解锁
//                String tokenOld = stringRedisTemplate.opsForValue().get("lock");
//                if(token.equals(tokenOld)) {
//                    stringRedisTemplate.delete("lock");
//                }
                String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";

                log.info("加锁完成开始删除reids锁");
                Long isDelete = stringRedisTemplate.execute(new DefaultRedisScript<>(lua, Long.class),
                        Collections.singletonList("lock"), token);

                return catalogJsonFromDBData;
            } else {
                //加锁失败 自旋重试
                log.info("redis分布式锁加锁失败,将进行自旋重试");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getCatalogJsonFromDB();
            }
        }
    }


    // 使用redisson进行分布式锁

    /**
     * 缓存数据一致性
     * 双写一致性和失效一致性
     * 双写 : 在更新数据库后更新缓存
     * 失效 : 更新数据库后删掉缓存
     * */


    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {

        synchronized (this) {
            RLock lock = redissonClient.getLock("catalogJson-lock");
            lock.lock();
            Map<String, List<Catelog2Vo>> catalogJsonFromDBData;
            try {
                log.info("redis分布式锁加锁成功");
                catalogJsonFromDBData = getCatalogJsonFromDBData();
            } finally {
                log.info("加锁完成开始删除reids锁");
                lock.unlock();
            }
            return catalogJsonFromDBData;
        }
    }

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBDataWithSpringCache() {

        log.error("缓存穿透,开始查询数据库...查询数据库线程: {}",Thread.currentThread().getName());
        List<CategoryEntity> allCategoryEntityList = baseMapper.selectList(null);

        // 查出所有1级分类
        List<CategoryEntity> level1Categorys = getParent_cid(allCategoryEntityList,0L);
        Map<String, List<Catelog2Vo>> catalogJsonMap = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
            // 封装为二级分类
            List<CategoryEntity> level2Categorys = getParent_cid(allCategoryEntityList, l1.getCatId());
            if (ObjectUtils.isNotEmpty(level2Categorys)) {
                return level2Categorys.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo =
                            new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 获得三级分类
                    List<CategoryEntity> level3Categorys = getParent_cid(allCategoryEntityList, l2.getCatId());
                    if (ObjectUtils.isNotEmpty(level3Categorys)) {
                        List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Categorys.stream().map(l3 -> new Catelog2Vo.Catelog3Vo(
                                l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()
                        )).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3Vos);
                    }
                    return catelog2Vo;

                }).collect(Collectors.toList());
            } else {
                // 集合中没有数据的话返回一个空集合
                return new ArrayList<>();
            }
        }));

        return catalogJsonMap;
    }

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBData() {
        // 分布式锁性能要差一些,若不是强一致性的比如这里的场景可以用本地锁
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if(StringUtils.isNotBlank(catalogJson)) {
            return JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        }
        log.error("缓存穿透,开始查询数据库...查询数据库线程: {}",Thread.currentThread().getName());
        List<CategoryEntity> allCategoryEntityList = baseMapper.selectList(null);

        // 查出所有1级分类
        List<CategoryEntity> level1Categorys = getParent_cid(allCategoryEntityList,0L);
        Map<String, List<Catelog2Vo>> catalogJsonMap = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
            // 封装为二级分类
            List<CategoryEntity> level2Categorys = getParent_cid(allCategoryEntityList, l1.getCatId());
            if (ObjectUtils.isNotEmpty(level2Categorys)) {
                return level2Categorys.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo =
                            new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 获得三级分类
                    List<CategoryEntity> level3Categorys = getParent_cid(allCategoryEntityList, l2.getCatId());
                    if (ObjectUtils.isNotEmpty(level3Categorys)) {
                        List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Categorys.stream().map(l3 -> new Catelog2Vo.Catelog3Vo(
                                l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()
                        )).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3Vos);
                    }
                    return catelog2Vo;

                }).collect(Collectors.toList());
            } else {
                // 集合中没有数据的话返回一个空集合
                return new ArrayList<>();
            }
        }));
        //转为json 放入缓存
        String jsonString = JSON.toJSONString(catalogJsonMap);
        // 添加过期时间
        stringRedisTemplate.opsForValue().set("catalogJson",jsonString,1, TimeUnit.DAYS);

        return catalogJsonMap;
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> allCategoryEntityList,Long parentCid) {
        return allCategoryEntityList.stream().filter(item-> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }

    private List<Long> findParentPath(Long catelogId,List<Long> paths) {
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if(byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(),paths);
        }
        return paths;
    }

    //2.3 我们使用递归来进行构建树,PS: 递归深的话容易栈溢出,但是我们这里就三级菜单所以可以这么干
    // 若菜单很深可以使用map索引以循环方式赋值
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> list) {
        return list.stream().filter(categoryEntity ->
                categoryEntity.getParentCid().equals(root.getCatId())
        ).peek(categoryEntity -> {
            categoryEntity.setChildren(getChildren(categoryEntity,list));
        }).sorted(Comparator.comparingInt(a -> (a.getSort() == null ? 0 : a.getSort()))).collect(Collectors.toList());
    }

}